#!/usr/bin/env python
# encoding: utf-8
"""
geohash64.py

Created by @a_dat on 2010-11-27.
MIT license.
"""

import timeit
import math

# __all__ = ['encode','decode','decode_exactly','bbox', 'neighbors', 'expand']
__all__ = ['encode', 'decode']

###############################################################################
# base64url character
###############################################################################

_base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
_base64_map = {}
for _i in range(len(_base64)):
    _base64_map[_base64[_i]] = _i


###############################################################################
# LatLon Class
###############################################################################

class LatLon():
    """汎用の緯度経度クラス"""

    def __init__(self, lat, lon):
        if not -90 <= lat <= 90:
            raise OutOfRangeError("'lat' is out of range.")
        if not -180 <= lon <= 180:
            raise OutOfRangeError("'lon' is out of range.")
        self.lat = float(lat)
        self.lon = float(lon)

    def __str__(self):
        return ("<geohash64.LatLon unit='degree'> {lat:%s, lon:%s}"
            % (self.lat, self.lon))

    def __add__(self, ll):
        return LatLon(
            self.lat + ll.lat,
            self.lon + ll.lon
        )

    def getGeoHash64(self, precision=10):
        """LatLon => GeoHash64"""

        if not 0 < precision:
            raise OutOfRangeError("'precision' is out of range.")

        lat = (self.lat + 90) / 180  # => 0.69091
        lon = (self.lon + 180) / 360  # => 0.22069

        hash = ""
        for i in range(precision):
            lat *= 8
            lon *= 8
            lat_int = int(lat)
            lon_int = int(lon)
            hash += _base64[(
                ((lat_int << 3) & 32)
                + ((lon_int << 2) & 16)
                + ((lat_int << 2) & 8)
                + ((lon_int << 1) & 4)
                + ((lat_int << 1) & 2)
                + ((lon_int << 0) & 1)
            )]

        return GeoHash64(hash)

    def distance_to(self, another_LatLon):
        return self.distance_from(another_LatLon)

    def distance_from(self, another_LatLon):
        """Hubeny's formula
        http://yamadarake.web.fc2.com/trdi/2009/report000001.html
        """

        if not another_LatLon.__class__.__name__ == "LatLon":
            raise InvalidArgumentError("Argument is not 'LatLon'")

        lat1_rad = self.lat * math.pi / 180
        lon1_rad = self.lon * math.pi / 180
        lat2_rad = another_LatLon.lat * math.pi / 180
        lon2_rad = another_LatLon.lon * math.pi / 180
        lat_average_rad = (lat1_rad + lat2_rad) / 2
        lat_diff = lat1_rad - lat2_rad
        lon_diff = lon1_rad - lon2_rad

        meridian = (6335439.327 /
            ((1 - 0.006694379990 * math.sin(lat_average_rad) ** 2) ** 3)
            ** 0.5)
        prime_vertical = (6378137.000 /
            (1 - 0.006694379990 * math.sin(lat_average_rad) ** 2)
            ** 0.5)
        return (((meridian * lat_diff) ** 2
            + (prime_vertical * math.cos(lat_average_rad) * lon_diff) ** 2)
            ** 0.5)

    @property
    def lat():
        return self.lat

    @property
    def lon():
        return self.lon

###############################################################################
# Coordinate Class
###############################################################################


class Coordinate():
    """
    """

    def __init__(self, n, e):
        self.n = n
        self.e = e

    def __str__(self):
        return ("<geohash64.Coordinate unit='meter'> {n: %s, e: %s}"
             % (self.n, self.e))

    @property
    def n(self):
        return self.n

    @property
    def e(self):
        return self.e


###############################################################################
# GeoHash64 Class
###############################################################################

class GeoHash64():
    """
    """

    def __init__(self, hash, precision=10):
        if hash.__class__.__name__ == "str":
            self._hash = hash
            self._precision = len(self._hash)
            self._hash2geo()

        else:
            raise "Argument is invalid"

    def __str__(self):
        return """<geohash64.GeoHash64> {
            hash: %s,
            center_ll: %s,
            south_west_ll: %s,
            error: %s,
            coordinate_error: %s,
            precision: %d
        }""" % (repr(self._hash),
            self.center_ll,
            self.south_west_ll,
            self.error,
            self.coordinate_error,
            self._precision)

    def _hash2geo(self):
        # self._hash = "3gLi"

        decimal_list = [_base64_map[s] for s in self._hash]  # s = "Z"

        lat = 0.0
        lon = 0.0
        for decimal in decimal_list[::-1]:
            # decimal = 25 = 0b011001
            lat += (decimal >> 3) & 4  # => 0b100
            lon += (decimal >> 2) & 4  # => 0b100
            lat += (decimal >> 2) & 2  # => 0b010
            lon += (decimal >> 1) & 2  # => 0b010
            lat += (decimal >> 1) & 1  # => 0b001
            lon += (decimal >> 0) & 1  # => 0b001
            lat /= 8  # >>=3
            lon /= 8  # >>=3

        self._ll = LatLon(lat * 180 - 90, lon * 360 - 180)

        self._set_error()

    def _set_error(self):
        self._error = LatLon(
            90.0 / 8 ** self._precision,
            180.0 / 8 ** self._precision
        )
        self._coordinate_error = Coordinate(
            self._ll.distance_to(
                LatLon(self._ll.lat + self._error.lat, self._ll.lon)
            ),
            self._ll.distance_to(
                LatLon(self._ll.lat, self._ll.lon + self._error.lon)
            )
        )

    @property
    def hash(self):
        """Return geohash64"""

        return self._hash

    @property
    def center_ll(self):
        """'ll' means 'Latitude Longitude'"""

        return self._ll + self._error

    @property
    def south_west_ll(self):
        """'ll' means 'Latitude Longitude'"""

        return self._ll

    @property
    def error(self):
        """Error in LatLon"""

        return self._error

    @property
    def coordinate_error(self):
        """Error in units of meter"""

        return self._coordinate_error

###############################################################################
# Exception Class
###############################################################################


class OutOfRangeError(Exception):
    pass


class InvalidArgumentError(Exception):
    pass


###############################################################################
# Public method
###############################################################################

def encode(ll, precision=10):
    """LatLon->hash"""
    return ll.getGeoHash64(precision=precision).hash


def decode(hash):
    """hash->LatLon"""
    return GeoHash64(hash).center_ll


###############################################################################
# main function
###############################################################################

def _main():
    """t= timeit.Timer(
        setup="import geohash64",
        stmt="geohash64.GeoHash64('Z0C3-_')")
    print "time: ", t.timeit(number = 10000)
    """

    print GeoHash64('w')
    print LatLon(0, 0).getGeoHash64(precision=3)
    print LatLon(35.026131, 135.780673).getGeoHash64(precision=7)

if __name__ == '__main__':
    _main()
